/*
 * Copyright (C) 2013  Bob Rutledge
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 * and open the template in the editor.
 */
package org.lreqpcr.core.data_objects;

import java.util.List;

/**
 * An average sample profile generated by averaging the Fc datasets produced by
 * replicates sample profiles. This increases Fc read precision which in turn
 * generates a more reliable analysis.
 *
 * @author Bob Rutledge
 */
public class AverageSampleProfile extends SampleProfile implements AverageProfile {

    private List<SampleProfile> sampleProfileList;
    private double avTm = 0;//The average amplicon melting temperature of the SampleProfiles
    private double repAvNo = -1;//This should also conform when Fmax normalization has been applied
    private double replicateScatterTolerance = 0.7;//The maximum C1/2 range of the replicate profiles allowed for valid average profile construction

    /**
     * An average sample profile constructed from its sample replicate profiles.
     */
    public AverageSampleProfile() {
        setChildClass(SampleProfile.class);
    }

    /**
     *
     * @return list of replicate sample profiles
     */
    public List<SampleProfile> getReplicateProfileList() {
        return sampleProfileList;
    }

    /**
     * Will throw an illegal cast exception if the list does not contain Sample
     * Profiles.
     *
     * @param replicate Profile list containing SampleProfiles
     */
    @SuppressWarnings(value = "unchecked")
    public void setReplicateProfileList(List<? extends Profile> replicateProfileList) {
        this.sampleProfileList = (List<SampleProfile>) replicateProfileList;
        calculateAvAmpTm();
    }

    /**
     * Returns the average melting temperature (Tm) of the SampleProfile(s)
     * amplicons. Note that this will return -1 if a Tm is not available.
     *
     * @return the average SampleProfile Tm or -1 if none is available
     */
    public double calculateAvAmpTm() {
        double tmSum = 0;
        int counter = 0;
        for (SampleProfile repProfile : sampleProfileList) {
            if (repProfile.isExcluded()) {
                continue;//Do nothing
            } else {
                if (repProfile.hasAnLreWindowBeenFound()) {
                    tmSum += repProfile.getAmpTm();
                    counter++;
                }
            }
        }
        if (counter != 0) {
            avTm = tmSum / counter;
        } else {
            avTm = -1;
        }
        return avTm;
    }

    @Override
    public double getAmpTm() {
        return calculateAvAmpTm();
    }

    /**
     * Recalculates No depending on the state of this AverageProfile. If
     * ReplicateSampleProfile average No is less than 10 molecules, the
     * AverageProfile will be inactivated and No set to the ReplicateProfile
     * average No.
     */
    @Override
    public void updateSampleProfile() {
        //Without an OCF, No values cannot be calculated
        if (getOCF() >= 0) {
            isTheReplicateAverageNoLessThan10Molecules();//This simply updates the Replicate average No
        } else {
            no = -1;//Signifies no value vs. just zero
        }
        if (!isTheReplicateAverageNoLessThan10Molecules()) {
            super.updateSampleProfile();//Let the Sample Profile update No
        }
    }

    /**
     * Sort By Amplicon Name, then Sample name
     *
     * @param sampleProfile the Sample Profile to compare to
     * @return
     */
    @Override
    public int compareTo(Object sampleProfile) {
        SampleProfile profile = (AverageSampleProfile) sampleProfile;
        //Sort by name
        if (getAmpliconName().compareTo(profile.getAmpliconName()) == 0) {
            //They have the same Sample name
            //Sort by Amplicon name
            return getSampleName().compareTo(profile.getSampleName());
        }
        //They have different Sample names, to use Sample name to sort
        return getAmpliconName().compareTo(profile.getAmpliconName());
    }

    /**
     * Determines if the AverageProfile is valid, based on whether the number of
     * target molecules in the sample is greater than 10. If the number of
     * target molecules is less than 10, the resulting average profile becomes
     * distorted due to by Poison Distribution, generating extensive profile
     * scattering.
     * <p>
     * In this case, the AverageProfile inherits the target quantities
     * determined by the replicate profiles, that is the average from the
     * replicate profiles.
     *
     * @return whether the average No is less than 10 molecules
     */
    public boolean isTheReplicateAverageNoLessThan10Molecules() {
        if (getReplicateProfileList() == null) {
            //Occurs during data import
            return false;
        }
        //Update the replicate average No
        getReplicatePrfAvNo();
        if (repAvNo < 10) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Depending on whether this is a valid Average Profile, this function will
     * return the the number of target molecules determined from the average
     * amplification profile. Note that getNo() is overridden to call this
     * function.
     *
     * @return the calculated number of target molecules (No) if this is a valid
     * average profile;
     * <p>
     * if No is less than 10 molecules, the average replicate No is returned;
     * <p>
     * if the replicate profiles are too scattered, -1 is returned.
     */
    public double getAvProfileNo() {
        if (getReplicatePrfAvNo() < 10) {
            //revert to the average replicate No
            return getReplicatePrfAvNo();
        }
        if (!areTheRepProfilesSufficientlyClustered()) {
            //A valid No is not available, to return -1
            return -1;
        }
        return super.getNo();
    }

    @Override
    public double getNo() {
        return getAvProfileNo();
    }

    //Override get Emax, midC and Fmax in order to test if this is a valid profile
    @Override
    public double getEmax() {
        if (!areTheRepProfilesSufficientlyClustered() || isTheReplicateAverageNoLessThan10Molecules()) {
            return -1;
        }
        return super.getEmax(); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public double getFmax() {
        if (!areTheRepProfilesSufficientlyClustered() || isTheReplicateAverageNoLessThan10Molecules()) {
            return -1;
        }
        return super.getFmax(); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public double getMidC() {
        return super.getMidC(); //To change body of generated methods, choose Tools | Templates.
    }

    //Override getWellLabel to allow single replicate profile well labels to be returned
    @Override
    public String getWellLabel() {
        if (getTheNumberOfActiveReplicateProfiles() == 1) {
            for (Profile profile : sampleProfileList) {
                if (!profile.isExcluded() && profile.hasAnLreWindowBeenFound()) {
                    return profile.getWellLabel();
                }
            }
        }
        return "Multiple";
    }

    public int getTheNumberOfActiveReplicateProfiles() {
        int numberOfActiveReplicateProfiles = 0;
        for (Profile profile : sampleProfileList) {
            if (!profile.isExcluded() && profile.hasAnLreWindowBeenFound()) {
                numberOfActiveReplicateProfiles++;
            }
        }
        return numberOfActiveReplicateProfiles;
    }

    /**
     * Determines whether the replicate profiles are sufficiently clustered to
     * generate a valid Fc dataset that was used to create this average profile.
     * If sufficient clustering is not apparent, the target quantity reverts to
     * the average replicate No and the Profile is excluded. If true, the 
     * isExcluded is set to false.
     *
     * @return whether this is a valid average profile
     */
    public boolean areTheRepProfilesSufficientlyClustered() {
        //No issue if there is only one replicate profile
        if (getTheNumberOfActiveReplicateProfiles() == 1) {
            return true;
        }
        //Determine if the replicate C1/2 values spread is >replicateScatterTolerance
        //Determine the 2 most different C1/2 values
        Profile highest = null;
        Profile lowest = null;
        for (Profile prf : getReplicateProfileList()) {
            if (prf.hasAnLreWindowBeenFound() && !prf.isExcluded()) {
                if (highest == null) {
                    highest = prf;
                }
                if (prf.getMidC() >= highest.getMidC()) {
                    //prf has the higher C1/2, so check to see if it is higher than lowest
                    if (lowest != null) {
                        if (lowest.getMidC() > highest.getMidC()) {
                            lowest = highest;
                        }
                    } else {
                        lowest = highest;
                    }
                    highest = prf;
                } else {//prf is lower, so check if it is lower that lowest
                    if (lowest != null) {
                        if (lowest.getMidC() > prf.getMidC()) {
                            lowest = prf;
                        }
                    } else {//No lower has been identified, so make prf the lowest
                        lowest = prf;
                    }
                }
            }
        }
        if (replicateScatterTolerance == 0) {//Profile predates scatter tolerance testing
            replicateScatterTolerance = 0.7;//Default value at the time of first implementation
            //Future versions should included the ability for the user to set the scatter tolerance
        }
        if (highest != null && lowest != null) {//This should never be false
            if (highest.getMidC() - lowest.getMidC() > replicateScatterTolerance) {
                //This is an invalide average profile
                setHasAnLreWindowBeenFound(false);
                return false;
            } else {
                return true;
            }
        }
        return true;//This should never be reached as this should only occur when there is one replicate profile
    }

    /**
     * Returns the maximum allowable difference in C1/2 across the replicate
     * profiles for determining if a valid average profile can be constructed.
     *
     * @return the maximum range of C1/2 values
     */
    public double getReplicateScatterTolerance() {
        return replicateScatterTolerance;
    }

    /**
     * Sets the maximum allowable difference in C1/2 across the replicate
     * profiles for determining if a valid average profile can be constructed.
     *
     * @param replicateScatterTolerance
     */
    public void setReplicateScatterTolerance(double replicateScatterTolerance) {
        this.replicateScatterTolerance = replicateScatterTolerance;
    }

    /**
     * An alternative method for determining the number of target molecules is
     * to average the No values generated by the replicate profiles. This is
     * primarily used the replicate profiles are not sufficiently clustered to
     * generate a valid average profile, such as when the target quantity is
     * less than 10 molecules.
     *
     * @return the updates average replicate target quantity expressed in
     * molecules
     */
    public double getReplicatePrfAvNo() {
        double sum = 0;
        int counter = 0;
        for (SampleProfile repPrf : getReplicateProfileList()) {
            //It is important not to include excluded profiles
            if (!repPrf.isExcluded()) {
                if (!repPrf.hasAnLreWindowBeenFound()) {
//Without an LRE Window, a valid LRE-derived avNo is not available
//However, such profiles (i.e. flat profiles) default to zero molecules and thus must be counted
                    counter++;
                } else {
                    sum += repPrf.getNo();
                    counter++;
                }
            }
        }
        if (counter == 0) {
            //No replicate profiles are avaiable
            //This should never happen
            repAvNo = -1;
        } else {
            repAvNo = sum / counter;
        }
        return repAvNo;
    }
}
